﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Drawing;
using System.IO;
using System.Timers;
using Emgu.CV;
using Emgu.CV.Structure;

namespace ObjRec
{
    public class FaceRec
    {
        #region Variables
        private CascadeClassifier _cc;
        private FaceRecognizer _rec;
        private Capture _cap;
        public bool _isTrained;
        private ArduinoConnect _arduino;
        private Timer _timer;
        private int _clock;
        private bool _faceRecognized;
        private Dictionary<int, int> _binTable;
        private Size _rescaledFaceSize, _minFaceSize, _maxFaceSize;

        public Dictionary<int, Person> _people;
        public Image<Gray, byte> _LBPImg;
        public Image<Gray, byte> _uniImg;
        public Image<Gray, byte> _nonuniImg;
        public Bitmap _originalImage;
        public Image<Gray, byte>[] _faces;
        #endregion

        public FaceRec()
        {
            _rescaledFaceSize = new Size(ExternalFunctions.RESCALED_FACE_SIZE, ExternalFunctions.RESCALED_FACE_SIZE);
            _minFaceSize = new Size(ExternalFunctions.MIN_FACESIZE, ExternalFunctions.MIN_FACESIZE);
            _maxFaceSize = new Size(ExternalFunctions.MAX_FACE_SIZE, ExternalFunctions.MAX_FACE_SIZE);
            _cc = new CascadeClassifier(ExternalFunctions.GetPath + "\\haarcascade_frontalface_default.xml");
            _cap = new Capture();
            _rec = new LBPHFaceRecognizer(ExternalFunctions.LBP_RADIUS, ExternalFunctions.LBP_NEIGHBOR, ExternalFunctions.LBP_GRID_X, ExternalFunctions.LBP_GRID_Y, ExternalFunctions.LBP_THRESHOLD);
            _isTrained = false;
            _people = new Dictionary<int, Person>();
            _arduino = new ArduinoConnect("COM5");
            _LBPImg = new Image<Gray, byte>(_rescaledFaceSize.Width - 2 * ExternalFunctions.LBP_RADIUS, _rescaledFaceSize.Height - 2 * ExternalFunctions.LBP_RADIUS);
            _uniImg = new Image<Gray, byte>(_rescaledFaceSize.Width - 2 * ExternalFunctions.LBP_RADIUS, _rescaledFaceSize.Height - 2 * ExternalFunctions.LBP_RADIUS);
            _nonuniImg = new Image<Gray, byte>(_rescaledFaceSize.Width - 2 * ExternalFunctions.LBP_RADIUS, _rescaledFaceSize.Height - 2 * ExternalFunctions.LBP_RADIUS);
            _binTable = new Dictionary<int, int>();
            for (int i = 0, j = 1; i < (int)Math.Pow(2, ExternalFunctions.LBP_NEIGHBOR); i++)
            {
                if (!_binTable.ContainsKey(i))
                {
                    if (ExternalFunctions.isUniform(i))
                    {
                        _binTable.Add(i, j);
                        j++;
                    }
                    else _binTable.Add(i, 0);
                }
            }
        }

        public void LoadFile()
        {
            _people = new Dictionary<int,Person>();
        }

        public Bitmap Stream
        {
            get
            {
                Image<Bgr, byte> img = _cap.QueryFrame();
                double scale = 1.0 * ExternalFunctions.IMAGE_SIZE / Math.Min(img.Height, img.Width);
                if (scale >= 1) return img.Bitmap;
                else return img.Resize(scale, Emgu.CV.CvEnum.INTER.CV_INTER_AREA).Bitmap;
            }
        }

        public void Scan()
        {
            Image<Gray, byte> grayImage = new Image<Gray, byte>(_originalImage);
            Rectangle[] rects;
            rects = _cc.DetectMultiScale(grayImage, ExternalFunctions.SCALE_FACTOR, ExternalFunctions.MIN_NEIGHBOR, _minFaceSize, _maxFaceSize);
            if (rects.Length > 0)
            {
                _faces = new Image<Gray, byte>[rects.Length];
                for (int i = 0; i < _faces.Length; i++)
                {
                    Image<Gray, byte> tmp = grayImage.Copy();
                    tmp.ROI = rects[i];
                    _faces[i] = tmp.Copy().Resize(_rescaledFaceSize.Width, _rescaledFaceSize.Height, Emgu.CV.CvEnum.INTER.CV_INTER_AREA);
                    Graphics.FromImage(_originalImage).DrawRectangle(new Pen(Color.Red, 2), rects[i]);
                    if (_isTrained)
                    {
                        string name = Recognize(_faces[i]);
                        if (name != "")
                        {
                            _faceRecognized = true;
                        }
                        else
                        {
                            _faceRecognized = false;
                        }
                        Graphics.FromImage(_originalImage).DrawString(name + " " + _clock.ToString(),
                            new Font(FontFamily.GenericMonospace, 20),
                            new SolidBrush(Color.LightGreen),
                            rects[i].Location);
                    }
                }
                CalculateLBPFace();
            }
            else _faceRecognized = false;
            Graphics.FromImage(_originalImage).DrawString(_originalImage.Width + "x" + _originalImage.Height, 
                new Font(FontFamily.GenericMonospace, 20), 
                new SolidBrush(Color.Yellow), 
                0, 
                0);
        }

        public Dictionary<int, Person> DetectFaces(Bitmap bmp)
        {
            Dictionary<int, Person> people = new Dictionary<int, Person>();
            Image<Gray, byte> tmp1 = new Image<Gray, byte>(bmp);
            double scale = 1.0 * ExternalFunctions.IMAGE_SIZE / Math.Min(tmp1.Height, tmp1.Width);
            Image<Gray, byte> grayImage;
            if (scale >= 1) grayImage = tmp1;
            else grayImage = tmp1.Resize(scale, Emgu.CV.CvEnum.INTER.CV_INTER_AREA);
            Rectangle[] rects;
            rects = _cc.DetectMultiScale(grayImage, ExternalFunctions.SCALE_FACTOR, ExternalFunctions.MIN_NEIGHBOR, new Size(32, 32), _maxFaceSize);
            if (rects.Length > 0)
            {
                Image<Gray, byte>[] faces = new Image<Gray, byte>[rects.Length];
                for (int i = 0; i < faces.Length; i++)
                {
                    Image<Gray, byte> tmp = grayImage.Copy();
                    tmp.ROI = rects[i];
                    faces[i] = tmp.Copy().Resize(_rescaledFaceSize.Width, _rescaledFaceSize.Height, Emgu.CV.CvEnum.INTER.CV_INTER_AREA);
                    string name = "";
                    if (_isTrained)
                    {
                        name = Recognize(faces[i]);
                    }
                    Person p = new Person();
                    p._name = name;
                    p._image = ExternalFunctions.BitmapToBytes(faces[i].Bitmap);
                    people.Add(i, p);
                }
            }
            return people;
        }

        public void Train(bool saveToFile)
        {
            _isTrained = false;
            int[] index_list = _people.Keys.ToArray<int>();
            if (index_list.Length == 0) return;
            Image<Gray, byte>[] images = new Image<Gray, byte>[_people.Count];
            ArrayList lst = new ArrayList();
            foreach (KeyValuePair<int, Person> p in _people)
            {
                lst.Add(new Image<Gray, byte>(ExternalFunctions.BytesToBitmap(p.Value._image)));
            }
            lst.CopyTo(images, 0);
            _rec.Train(images, index_list);
            if (saveToFile) _rec.Save("Trained.xml");
            _isTrained = true;
        }

        public string Recognize(Image<Gray, byte> face)
        {
            string s = "";
            FaceRecognizer.PredictionResult r = _rec.Predict(face);
            if (r.Label != -1) s = (_people[r.Label] as Person)._name;
            return s;
        }

        public Bitmap GetFace(int index)
        {
            if (_faces.Length == 0) throw new Exception("No face(s) detected!");
            else
            {
                return _faces[index].Bitmap;
            }
        }

        public void AddPerson(string name, Bitmap bmp)
        {
            Person p = new Person();
            p._name = name;
            p._image = ExternalFunctions.BitmapToBytes(bmp);
            if (_people.Count == 0)
            {
                _people.Add(0, p);
            }
            else
            {
                int lastkey = (_people as Dictionary<int, Person>).Keys.Last();
                lastkey++;
                _people.Add(lastkey, p);
            }
            Train(false);
        }

        public void OpenConnection()
        {
            if (_arduino.Connect()) StartTimer();
        }

        private void StartTimer()
        {
            _timer = new Timer(100);
            _clock = 0;
            _timer.Elapsed += _timer_Elapsed;
            _timer.Start();
        }

        private void _timer_Elapsed(object sender, ElapsedEventArgs e)
        {
            if (_faceRecognized) _clock += 5;
            else _clock -= 5;
            _clock = ExternalFunctions.Clamp(_clock, 0, 100);
            if (_clock == 100) _arduino.Send("o");
            if (_clock == 0) _arduino.Send("c");
        }

        private void CalculateLBPFace()
        {
            int k;
            for (int i = ExternalFunctions.LBP_RADIUS; i < _faces[0].Rows - ExternalFunctions.LBP_RADIUS; i++)
            {
                for (int j = ExternalFunctions.LBP_RADIUS; j < _faces[0].Cols - ExternalFunctions.LBP_RADIUS; j++)
                {
                    double center = _faces[0][i, j].Intensity;
                    k = 0;
                    k |= _faces[0][i, j + ExternalFunctions.LBP_RADIUS].Intensity >= center ? 1 : 0;
                    k <<= 1;
                    k |= _faces[0][i + ExternalFunctions.LBP_RADIUS, j + ExternalFunctions.LBP_RADIUS].Intensity >= center ? 1 : 0;
                    k <<= 1;
                    k |= _faces[0][i + ExternalFunctions.LBP_RADIUS, j].Intensity >= center ? 1 : 0;
                    k <<= 1;
                    k |= _faces[0][i + ExternalFunctions.LBP_RADIUS, j - ExternalFunctions.LBP_RADIUS].Intensity >= center ? 1 : 0;
                    k <<= 1;
                    k |= _faces[0][i, j - ExternalFunctions.LBP_RADIUS].Intensity >= center ? 1 : 0;
                    k <<= 1;
                    k |= _faces[0][i - ExternalFunctions.LBP_RADIUS, j - ExternalFunctions.LBP_RADIUS].Intensity >= center ? 1 : 0;
                    k <<= 1;
                    k |= _faces[0][i - ExternalFunctions.LBP_RADIUS, j].Intensity >= center ? 1 : 0;
                    k <<= 1;
                    k |= _faces[0][i - ExternalFunctions.LBP_RADIUS, j + ExternalFunctions.LBP_RADIUS].Intensity >= center ? 1 : 0;
                    _LBPImg[i - ExternalFunctions.LBP_RADIUS, j - ExternalFunctions.LBP_RADIUS] = new Gray(k);
                    if (ExternalFunctions.isUniform(k))
                    {
                        _uniImg[i - ExternalFunctions.LBP_RADIUS, j - ExternalFunctions.LBP_RADIUS] = new Gray(k);
                    }
                    else _nonuniImg[i - ExternalFunctions.LBP_RADIUS, j - ExternalFunctions.LBP_RADIUS] = new Gray(k);
                }
            }
        }

        private double[] LocalHistogram(Image<Gray, byte> source)
        {
            double[] _histogram = new double[ExternalFunctions.CountUniform(ExternalFunctions.LBP_NEIGHBOR) + 1];
            for (int i = ExternalFunctions.LBP_RADIUS; i < source.Rows - ExternalFunctions.LBP_RADIUS; i++)
            {
                for (int j = ExternalFunctions.LBP_RADIUS; j < source.Cols - ExternalFunctions.LBP_RADIUS; j++)
                {
                    int k = 0;
                    double center = source[i, j].Intensity;
                    k |= source[i, j + ExternalFunctions.LBP_RADIUS].Intensity >= center ? 1 : 0;
                    k <<= 1;
                    k |= source[i + ExternalFunctions.LBP_RADIUS, j + ExternalFunctions.LBP_RADIUS].Intensity >= center ? 1 : 0;
                    k <<= 1;
                    k |= source[i + ExternalFunctions.LBP_RADIUS, j].Intensity >= center ? 1 : 0;
                    k <<= 1;
                    k |= source[i + ExternalFunctions.LBP_RADIUS, j - ExternalFunctions.LBP_RADIUS].Intensity >= center ? 1 : 0;
                    k <<= 1;
                    k |= source[i, j - ExternalFunctions.LBP_RADIUS].Intensity >= center ? 1 : 0;
                    k <<= 1;
                    k |= source[i - ExternalFunctions.LBP_RADIUS, j - ExternalFunctions.LBP_RADIUS].Intensity >= center ? 1 : 0;
                    k <<= 1;
                    k |= source[i - ExternalFunctions.LBP_RADIUS, j].Intensity >= center ? 1 : 0;
                    k <<= 1;
                    k |= source[i - ExternalFunctions.LBP_RADIUS, j + ExternalFunctions.LBP_RADIUS].Intensity >= center ? 1 : 0;
                    _histogram[_binTable[k]]++;
                }
            }
            return new Vector(_histogram).Normalized;
        }

        public double[] GlobalHistogram(Bitmap bmp)
        {
            Image<Gray, byte> _face;
            double[] _histogram;
            double[] _globalHistogram = new double[(ExternalFunctions.CountUniform(ExternalFunctions.LBP_NEIGHBOR) + 1) * (ExternalFunctions.LBP_GRID_X * ExternalFunctions.LBP_GRID_Y + 1)];
            Size _cellSize = new Size(_rescaledFaceSize.Width / ExternalFunctions.LBP_GRID_X, _rescaledFaceSize.Height / ExternalFunctions.LBP_GRID_Y);
            Rectangle _roi;
            int index = 0;
            for (int y = 0; y < ExternalFunctions.LBP_GRID_Y; y++)
            {
                for (int x = 0; x < ExternalFunctions.LBP_GRID_X; x++)
                {
                    _roi = new Rectangle(x * _cellSize.Width, y * _cellSize.Height, _cellSize.Width, _cellSize.Height);
                    _face = new Image<Gray, byte>(bmp);
                    _face.ROI = _roi;
                    Image<Gray, byte> _subImg = _face.Copy();
                    _histogram = LocalHistogram(_subImg);
                    Array.Copy(_histogram, 0, _globalHistogram, index, _histogram.Length);
                    index += ExternalFunctions.CountUniform(ExternalFunctions.LBP_NEIGHBOR) + 1;
                }
            }
            _face = new Image<Gray, byte>(bmp);
            _histogram = LocalHistogram(_face);
            Array.Copy(_histogram, 0, _globalHistogram, index, _histogram.Length);
            return _globalHistogram;
        }
    }
}
